目的:讀者可以讓 GUI 程式在使用過程中生成 UI 元件,並瞭解如何設計一個單一窗口管理與讀取這些額外生成之 UI 元件
我們在 day 13 中知道可以使用 QScrollArea 等容器來放置大量放置 UI 物件,且有提到 QScrollArea 可自動產生卷軸,因此適合放置 GUI 程式執行期間會額外生成的 UI 元件。
在今天的文章中,我們從抓取檔案路徑開始,實作一個可以依照選取資料數量、自動產生QCheckBox 的單一窗口。
在 day 9 中,我們使用 QT designer 的目的就是要用畫面去建構人看的畫面、而不要用文字去建構。但在以下情況還是需要用 code 在執行期間自動產生元件:
UI 元件的內容與 GUI 程式執行時產生的資料高度相關。例如選擇檔案後產生 QCheckBox 讓 user 選擇那些 wafer 需要分析、依照不同產品別生成不同 SEPC 輸入框讓使用者容易輸入
重複性高、高度結構化的內容,例如多組量測項目的最大/ 小值輸入框。我們可以利用自動生成元件減少製作 UI 時的工作量 (就是少拉幾次元件啦)
動態生成的元件的特點在於它們沒有一個像 button_start 這樣明確的名稱,但它們是真實的存在於記憶體與畫面中。因此在今天的文章中我們會設計單一窗口 (就是 class) 來統一管理這些執行時生成的額外 UI 元件。
我們都很習慣在使用 office 軟體時,從按下”開啟舊檔”後彈出的視窗中選擇檔案。而在 QT 環境中我們可以呼叫 QFileDialog.getOpenFileNames(……) 讓 user 可以在相同的視窗中選擇檔案。getOpenFileNames 是多選,選擇後會回傳檔案路徑清單與 filter (不重要)。
檔案路徑清單的型別是 list[str],若需要轉成 list[Path] 需自行處理。
getOpenFileNames的個別引數意義在此
file_names, _ = QFileDialog.getOpenFileNames(
parent=self.ui_window.centralwidget,
caption="選取檢測檔案",
dir="",
filter="CSV 檔案 (*.csv);;所有檔案 (*)",
)
除了多選檔案以外, QT 還提供多種檔案選擇方式:
選擇一個檔案 (開啟舊檔):QFileDialog.getOpenFileName (跟上面的少一個 s)
選擇一個路徑 + 輸入名稱 (另存新檔): QFileDialog.getSaveFileName
我們今天的目的就是藉由程式執行期間生成的資料(也就是上面產生的檔案路徑清單),產生動態元件:寫著 wafer ID 的 QCheckBox。這些物件可以讓 GUI 程式計算 user 選擇的檔案,不用重複選擇檔案讓程式更加靈活。
今天會示範做出以下功能之 GUI 程式:使用者讀檔後生成 ,並且 user 按下執行時,程式可以依照 function 與使用者選擇做出對應行為。在本案例中的呈現方式是顯示選擇檔案的路徑。

這個管理器負責生成 UI 元件並放入指定容器中,因此管理器需要記得以下 3 個資訊,其中容器與佈局是從外面傳進來。
容器 (container):生成的元件需要有一個容器來管理它,這裡會是傳入 QScrollArea 的子 QWidget
布局 (layout):指定生成物件擺放是直的還是橫的,可以在 QT designer 中預先設定
物件清單:存放在這個 class 中生成的物件,在這裡我們使用 tuple[QCheckBox, str] 儲存是方便讀取 QCheckBox 的狀態後,輸出旁邊的檔案名稱
class DynamicCheckboxManager:
def __init__(self, layout: QLayout, container: QWidget) -> None:
self._layout = layout
self._container = container
self._items: list[tuple[QCheckBox, str]] = []
我們設定的使用方式是先建立這個管理器,然後呼叫生成元件 api 建立動態元件。這個 api 的流程如下:
移除舊元件 → 建立新元件 → 新元件放入 layout → 新元件放入物件清單
其中在移除舊元件時,我們需要將舊元件從 layout 以及記憶體中移除。從 layout 中移除可以使用 layout.removeWidget(物件),這個移除只是讓 user 看不到而已、物件還存在在記憶體之中。要將物件乾淨移除,我們還需要呼叫 物件.deleteLater() 將物件從記憶體刪除。
關於
deleteLater()的意義,在 day 26 會有更詳盡的說明
def set_checkbox(self, parts: Iterable[object]) -> None:
"""根據傳入之資料設定 check box 於指定 layout 內"""
self.clear()
for part in parts:
_part = str(part)
checkbox = QCheckBox(_part, self._container)
self._layout.addWidget(checkbox)
self._items.append((checkbox, _part))
def clear(self) -> None:
"""把 layout 內的 check box 全部清空"""
if len(self._items) == 0:
return
for cb, _ in self._items:
self._layout.removeWidget(cb)
cb.deleteLater()
self._items.clear()
取值時我們也只需要呼叫取值 api 即可。取值 api 會設計成只回傳乾淨的使用者資料,不要回傳過程中生成的物件資訊。Controller 不需要知道這些物件,只需要知道使用者選擇 /輸入甚麼並串接到下一個對應的物件。
以本案例來說,只會回傳被勾選的項目名稱清單。
def get_checked_parts(self) -> list[str]:
return [
part
for checkbox, part in self._items
if checkbox.isChecked()
]
這個管理器的使用方式就跟 day 19 的管理器一樣,我們先在 controller 中將其實例化,另外管理器前記得加上 self.。
class MainController:
def __init__(self, ui_window: Ui_MainWindow) -> None:
self.ui_window = ui_window
# 存放片號與對應路徑之dict,方便接收使用者選擇後取出對應 path
self.path_dict: dict[str, Path] = {}
# 建立管理器
self.checkbox_obj = DynamicCheckboxManager(
self.ui_window.verticalLayout_sa, self.ui_window.sa_contents)
我們的示範例中有 2 個按鍵,其中一個是選擇檔案。我們可以將 getOpenFileNames 輸出的路徑字串清單轉換成等一下方便檢索的 dict[str, Path] 格式。並且在這 function 中我們也薑片號清單傳入生成元件 api 建立動態物件。
def get_file_list_and_set_checkbox(self):
"""選擇檔案後,將檔案名稱之 checkbox 加到 UI 中"""
file_names, _ = QFileDialog.getOpenFileNames(
parent=self.ui_window.centralwidget,
caption="選取檢測檔案",
dir="",
filter="CSV 檔案 (*.csv);;所有檔案 (*)",
)
# 取消選檔時保留目前畫面與清單
if not file_names:
return
# 將選擇結果寫入 property 內
_path_list = [
[Path(path_str).stem, Path(path_str)]
for path_str in file_names
]
self.path_dict = dict(_path_list)
# 加入 checkbox
item_list = list(self.path_dict.keys())
self.checkbox_obj.set_checkbox(item_list)
接下來是接收使用者選擇並做出對應行為。在這個示範例中我們是顯示使用者選擇的對應路徑,在專案實作時我們可以將檔名清單當篩選條件等多種應用。
def show_check_file_path(self):
"""把勾選的檔案對應路徑顯示於 UI 中"""
check_list = self.checkbox_obj.get_checked_parts()
msg_lines = []
self.ui_window.te_path_viewer.clear()
if not check_list:
return
for part in check_list:
path = self.path_dict[part]
msg_lines.append(f"Wafer {part} 的路徑: {path}")
self.ui_window.te_path_viewer.setText("\n".join(msg_lines))
剛剛設定的 function 記得與按鈕連接。
def make_connection(self):
self.ui_window.bn_get_path_list.clicked.connect(
self.get_file_list_and_set_checkbox
)
self.ui_window.bn_show_path.clicked.connect(
self.show_check_file_path
)
我們今天知道了如何在 GUI 程式執行時自動生成元件以及管理它們。
在明天 day 26 的文章中,我們要來處理一個使用者常遇到的問題:畫面卡住不動。我們會利用 QThread 來處理這種跑大型迴圈時,畫面就卡住不動的問題。